package gm.server;

import gm.GmLog;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.HashMap;

/**
 * Gép.
 *
 * @author imruf84
 */
public class Machine {

    /**
     * Motorok tárolója.
     */
    private final HashMap<String, Nut> nuts = new HashMap<>();
    /**
     * Fej.
     */
    private Head head;
    /**
     * Végpont.
     */
    private Tip tip;
    /**
     * Munkaterület.
     */
    private WorkingArea workingArea;
    /**
     * Bal motor neve.
     */
    public static String LEFT_MOTOR_NAME = "Bal";
    /**
     * Jobb motor neve.
     */
    public static String RIGHT_MOTOR_NAME = "Jobb";
    /**
     * Bal motor azonosítója.
     */
    public static String LEFT_MOTOR_ID = "L";
    /**
     * Jobb motor azonosítója.
     */
    public static String RIGHT_MOTOR_ID = "R";
    /**
     * Kulcsok nevei.
     */
    public static String TH_RODS_DISTANCE_KEY = "thRodsDistance";
    public static String TH_RODS_LENGTH_KEY = "thRodsLength";
    public static String TH_RODS_DIAMETER_KEY = "thRodDiameter";
    public static String TH_ROD_PITCH_KEY = "thRodPitch";
    public static String ROTATIONS_PER_MINUTE_KEY = "rotationsPerMinute";
    public static String STEPS_PER_ROTATION_KEY = "stepsPerRotation";
    public static String ARMS_LENGTH_KEY = "armsLength";
    public static String HEAD_DISTANCE_KEY = "headDistance";
    public static String NUT_DISTANCE_KEY = "nutDistance";
    public static String MOTOR_KEY = "motorSteps";
    public static String WORKING_AREA_CORNER1_KEY = "workingAreaCorner1";
    public static String WORKING_AREA_CORNER2_KEY = "workingAreaCorner2";
    /**
     * Menetes rudak közti távolság.
     */
    private double thRodsDistance;
    /**
     * Menetes rudak hossza.
     */
    private double thRodsLength;
    /**
     * Menetes rudak átmérője.
     */
    private double thRodDiameter;
    /**
     * Menetes rudak menetemelkedése.
     */
    private double thRodPitch;
    /**
     * Fordulat per másodperc.
     */
    private double rotationsPerMinute;
    /**
     * Lépés per fordulat.
     */
    private double stepsPerRotation;
    /**
     * Karok hossza.
     */
    private double armsLength;
    /**
     * Fejtől való távolság.
     */
    private double headDistance;
    /**
     * Csavartól való távolság.
     */
    private double nutDistance;
    /**
     * Beállításokat tartalmazó állomány neve.
     */
    private String configName = "";
    /**
     * Beállítások állományának a kapcsolata.
     */
    private Connection configConnection = null;
    /**
     * Első kapcsolódás.
     */
    private boolean firstConnect = true;
    /**
     * Lyuk tesztelő.
     */
    private HoleTester holeTester = null;

    /**
     * Konstruktor.
     *
     * @param thRodsDistance menetes rudak közti távolság
     * @param thRodsLength menetes rudak hossza
     * @param thRodDiameter menetes rudak átmérője
     * @param thRodPitch menetes rudak menetemelkedése
     * @param rotationsPerMinute motorok sebessége (körbefordulás per perc)
     * @param stepsPerRotation motorok lépés száma fordulatonként
     * @param armsLength karok hossza
     * @param headDistance végpont fejsől való távolsága
     * @param nutDistance végpont csavartól való távolsága
     * @param configName beállításokat tartalmazó állomány neve
     */
    public Machine(
            double thRodsDistance,
            double thRodsLength,
            double thRodDiameter,
            double thRodPitch,
            double rotationsPerMinute,
            double stepsPerRotation,
            double armsLength,
            double headDistance,
            double nutDistance,
            String configName) {

        this.thRodsDistance = thRodsDistance;
        this.thRodsLength = thRodsLength;
        this.thRodDiameter = thRodDiameter;
        this.thRodPitch = thRodPitch;
        this.rotationsPerMinute = rotationsPerMinute;
        this.stepsPerRotation = stepsPerRotation;
        this.armsLength = armsLength;
        this.headDistance = headDistance;
        this.nutDistance = nutDistance;
        this.configName = configName;

        // Ha van megadva állomány név akkor tároljuk az adatokat.
        if (!configName.isEmpty()) {
            try {
                connectToConfig();
                saveConfig();
                createMachineParts();
                addEvents();
                saveCurrentState();
            } catch (SQLException ex) {
                GmLog.printError(ex);
            }
        } else {
            createMachineParts();
            addEvents();
        }
    }

    /**
     * Konstruktor.
     *
     * @param thRodsDistance menetes rudak közti távolság
     * @param thRodsLength menetes rudak hossza
     * @param thRodDiameter menetes rudak átmérője
     * @param thRodPitch menetes rudak menetemelkedése
     * @param rotationsPerMinute motorok sebessége (körbefordulás per perc)
     * @param stepsPerRotation motorok lépés száma fordulatonként
     * @param armsLength karok hossza
     * @param headDistance végpont fejsől való távolsága
     * @param nutDistance végpont csavartól való távolsága
     */
    public Machine(
            double thRodsDistance,
            double thRodsLength,
            double thRodDiameter,
            double thRodPitch,
            double rotationsPerMinute,
            double stepsPerRotation,
            double armsLength,
            double headDistance,
            double nutDistance) {

        this(
                thRodsDistance,
                thRodsLength,
                thRodDiameter,
                thRodPitch,
                rotationsPerMinute,
                stepsPerRotation,
                armsLength,
                headDistance,
                nutDistance,
                ""
        );
    }

    /**
     * Konstruktor.
     *
     * @param configName beállításokat tartalmazó állomány neve
     */
    public Machine(String configName) {

        this.configName = configName;

        // Ha van megadva állomány név akkor betöltjük az adatokat.
        if (!configName.isEmpty()) {
            try {
                connectToConfig();
                loadConfig();
                createMachineParts();
                loadCurrentState();
                addEvents();
            } catch (SQLException ex) {
                GmLog.printError(ex);
            }
        }

    }

    /**
     * Események létrehozása.
     */
    private void addEvents() {
        MotorStepEvent me = new MotorStepEvent() {
            @Override
            public void onStep(Motor m) {
                try {
                    saveCurrentState();
                } catch (SQLException ex) {
                    GmLog.printError(ex);
                }
            }
        };
        getMotor(LEFT_MOTOR_ID).addOnStepEvent(me);
        getMotor(RIGHT_MOTOR_ID).addOnStepEvent(me);

        getWorkingArea().addOnCornerChangeEvent(new WorkingAreaCornerChangeEvent() {
            @Override
            public void onCornerChange(WorkingArea wa, int cornerIndex) {
                try {
                    saveCurrentState();
                } catch (SQLException ex) {
                    GmLog.printError(ex);
                }
            }
        });
    }

    /**
     * Csatlakozás a beállításokhoz, valamint a táblák létrehozása.
     *
     * @throws SQLException kivétel
     */
    private void connectToConfig() throws SQLException {

        // Ha van már kapcsolat, akkor kilépünk.
        if (null != configConnection) {
            return;
        }

        // Kapcsolat létrehozása.
        configConnection = DriverManager.getConnection("jdbc:h2:" + configName + ";trace_level_file=0;AUTO_SERVER=TRUE", "sa", "");

        // Első csatlakozás esetén zárjuk majd a kapcsolatot.
        if (firstConnect) {
            firstConnect = false;
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    if (null != configConnection) {
                        try {
                            configConnection.close();
                        } catch (SQLException ex) {
                            GmLog.printError(ex);
                        }
                    }
                }
            }));
        }

        // Táblák és adatok létrehozása.
        try (Statement stat = configConnection.createStatement()) {
            stat.execute("CREATE TABLE IF NOT EXISTS config (key VARCHAR(100), value VARCHAR(100))");
            storeValueIgnore(TH_RODS_DISTANCE_KEY, thRodsDistance + "");
            storeValueIgnore(TH_RODS_LENGTH_KEY, thRodsLength + "");
            storeValueIgnore(TH_RODS_DIAMETER_KEY, thRodDiameter + "");
            storeValueIgnore(TH_ROD_PITCH_KEY, thRodPitch + "");
            storeValueIgnore(ROTATIONS_PER_MINUTE_KEY, rotationsPerMinute + "");
            storeValueIgnore(STEPS_PER_ROTATION_KEY, stepsPerRotation + "");
            storeValueIgnore(ARMS_LENGTH_KEY, armsLength + "");
            storeValueIgnore(HEAD_DISTANCE_KEY, headDistance + "");
            storeValueIgnore(NUT_DISTANCE_KEY, nutDistance + "");

            // Motorok.
            for (String k : nuts.keySet()) {
                storeValueIgnore(k + MOTOR_KEY, getMotor(k).getCurrentStepsAbsolute() + "");
            }

            // Munkaterület.
            storeValueIgnore(WORKING_AREA_CORNER1_KEY, Arrays.toString(new double[]{0, 0}));
            storeValueIgnore(WORKING_AREA_CORNER2_KEY, Arrays.toString(new double[]{0, 0}));
        }

    }

    /**
     * Aktuális állapot tárolása.
     *
     * @throws SQLException kivétel
     */
    public final void saveCurrentState() throws SQLException {

        // Motorok.
        for (String k : nuts.keySet()) {
            storeValue(k + MOTOR_KEY, getMotor(k).getCurrentStepsAbsolute() + "");
        }

        // Munkaterület.
        storeValue(WORKING_AREA_CORNER1_KEY, Arrays.toString(getWorkingArea().getCorner1StepsAbsolute()));
        storeValue(WORKING_AREA_CORNER2_KEY, Arrays.toString(getWorkingArea().getCorner2StepsAbsolute()));
    }

    /**
     * Aktuális állapot beolvasása.
     *
     * @throws SQLException kivétel
     */
    public final void loadCurrentState() throws SQLException {

        // Motorok.
        for (String k : nuts.keySet()) {
            Motor m = getMotor(k);
            if (null != m) {
                m.setCurrentSteps(Double.parseDouble(getValue(k + MOTOR_KEY)), true);
            }
        }

        // Munkaterület.
        double steps1[] = toDoubleArray(getValue(WORKING_AREA_CORNER1_KEY));
        getWorkingArea().storeCorner1(steps1[0], steps1[1]);
        double steps2[] = toDoubleArray(getValue(WORKING_AREA_CORNER2_KEY));
        getWorkingArea().storeCorner2(steps2[0], steps2[1]);
    }

    /**
     * Karakterlánc átalakítása tömbbé.
     *
     * @param s karakterlánc
     * @return tömb
     */
    private double[] toDoubleArray(final String s) {

        String ls = s.substring(1, s.length() - 1);
        String sa[] = ls.split(",");

        double d[] = new double[sa.length];
        for (int i = 0; i < sa.length; i++) {
            d[i] = Double.parseDouble(sa[i].trim());
        }

        return d;
    }

    /**
     * Kulcs-érték pár tárolása ha nem létezne.
     *
     * @param key kulcs
     * @param value érték
     * @throws SQLException kivétel
     */
    private void storeValueIgnore(String key, String value) throws SQLException {

        if (null == configConnection) {
            return;
        }

        try (Statement stat = configConnection.createStatement()) {
            stat.execute("INSERT INTO config SELECT '" + key + "','" + value + "' FROM dual WHERE NOT EXISTS (SELECT * FROM config WHERE key = '" + key + "')");
        }
    }

    /**
     * Kulcshoz tartozó érték frissítése.
     *
     * @param key kulcs
     * @param value új érték
     * @throws SQLException kivétel
     */
    private void storeValue(String key, String value) throws SQLException {

        if (null == configConnection) {
            return;
        }

        try (Statement stat = configConnection.createStatement()) {
            stat.execute("UPDATE config SET value='" + value + "' WHERE key = '" + key + "'");
        }
    }

    /**
     * Kulcshoz tartozó érték beolvasása.
     *
     * @param key kulcs
     * @throws SQLException kivétel
     */
    private String getValue(String key) throws SQLException {

        if (null == configConnection) {
            return "NaN";
        }

        try (
                Statement stat = configConnection.createStatement();
                ResultSet r = stat.executeQuery("SELECT value FROM config WHERE key = '" + key + "'")) {

            if (!r.first()) {
                return null;
            }

            return r.getString("value");
        }

    }

    /**
     * Beállítások beolvasása állományból.
     */
    private void loadConfig() throws SQLException {
        thRodsDistance = Double.parseDouble(getValue(TH_RODS_DISTANCE_KEY));
        thRodsLength = Double.parseDouble(getValue(TH_RODS_LENGTH_KEY));
        thRodDiameter = Double.parseDouble(getValue(TH_RODS_DIAMETER_KEY));
        thRodPitch = Double.parseDouble(getValue(TH_ROD_PITCH_KEY));
        rotationsPerMinute = Double.parseDouble(getValue(ROTATIONS_PER_MINUTE_KEY));
        stepsPerRotation = Double.parseDouble(getValue(STEPS_PER_ROTATION_KEY));
        armsLength = Double.parseDouble(getValue(ARMS_LENGTH_KEY));
        headDistance = Double.parseDouble(getValue(HEAD_DISTANCE_KEY));
        nutDistance = Double.parseDouble(getValue(NUT_DISTANCE_KEY));
    }

    /**
     * Beállítások mentése állományba.
     *
     * @throws SQLException kivétel
     */
    private void saveConfig() throws SQLException {
        storeValue(TH_RODS_DISTANCE_KEY, thRodsDistance + "");
        storeValue(TH_RODS_LENGTH_KEY, thRodsLength + "");
        storeValue(TH_RODS_DIAMETER_KEY, thRodDiameter + "");
        storeValue(TH_ROD_PITCH_KEY, thRodPitch + "");
        storeValue(ROTATIONS_PER_MINUTE_KEY, rotationsPerMinute + "");
        storeValue(STEPS_PER_ROTATION_KEY, stepsPerRotation + "");
        storeValue(ARMS_LENGTH_KEY, armsLength + "");
        storeValue(HEAD_DISTANCE_KEY, headDistance + "");
        storeValue(NUT_DISTANCE_KEY, nutDistance + "");
    }

    /**
     * Alkatrészek létrehozása.
     */
    private void createMachineParts() {

        Motor m[] = createMotors();

        Motor motorLeft = m[0];
        Motor motorRight = m[1];

        ThreadedRod thRodLeft = new ThreadedRod(thRodsLength, thRodDiameter, thRodPitch, thRodsDistance / 2d, motorLeft);
        ThreadedRod thRodRight = new ThreadedRod(thRodsLength, thRodDiameter, thRodPitch, -thRodsDistance / 2d, motorRight);

        Nut nutLeft = new Nut(thRodLeft);
        nuts.put(LEFT_MOTOR_ID.toUpperCase(), nutLeft);
        Nut nutRight = new Nut(thRodRight);
        nuts.put(RIGHT_MOTOR_ID.toUpperCase(), nutRight);

        Arm armLeft = new Arm(armsLength, thRodDiameter, nutLeft);
        Arm armRight = new Arm(armsLength, thRodDiameter, nutRight);

        head = new Head(armLeft, armRight);

        tip = new Tip(nutLeft, nutDistance, head, headDistance);

        workingArea = new WorkingArea(tip);
    }

    /**
     * Motorok létrehozása.
     *
     * @return motorok tömbje
     */
    protected Motor[] createMotors() {

        Motor motorLeft = new Motor(LEFT_MOTOR_ID, LEFT_MOTOR_NAME, getRotationsPerMinute(), getStepsPerRotation());
        Motor motorRight = new Motor(RIGHT_MOTOR_ID, RIGHT_MOTOR_NAME, getRotationsPerMinute(), getStepsPerRotation());

        return new Motor[]{motorLeft, motorRight};

    }

    /**
     * Csavarok tárolójának a lekérdezése.
     *
     * @return csavarok tárolója
     */
    protected HashMap<String, Nut> getNutsContainer() {
        return nuts;
    }

    /**
     * Csavar lekérdezése.
     *
     * @param motorID motor azonosítója
     * @return csavar
     */
    public Nut getNut(final String motorID) {

        String ID = motorID.toUpperCase();

        if (nuts.containsKey(ID)) {
            return nuts.get(motorID.toUpperCase());
        }

        return null;
    }

    /**
     * Menetes rúd lekérdezése.
     *
     * @param motorID motor azonosítója
     * @return menetes rúd
     */
    public ThreadedRod getThreadedRod(final String motorID) {

        Nut n = getNut(motorID);

        if (null != n) {
            return n.getRod();
        }

        return null;
    }

    /**
     * Motor lekérdezése név alapján.
     *
     * @param ID azoniosító
     * @return motor
     */
    public Motor getMotor(final String ID) {

        ThreadedRod r = getThreadedRod(ID);

        if (null != r) {
            return r.getMotor();
        }

        return null;
    }

    /**
     * Bal motor lekérdezése.
     *
     * @return bal motor
     */
    public Motor getMotorLeft() {
        return getMotor(LEFT_MOTOR_ID);
    }

    /**
     * Jobb motor lekérdezése.
     *
     * @return jobb motor
     */
    public Motor getMotorRight() {
        return getMotor(RIGHT_MOTOR_ID);
    }

    /**
     * Fej lekérdezése.
     *
     * @return fej
     */
    public Head getHead() {
        return head;
    }

    /**
     * Végpont lekérdezése.
     *
     * @return végpont
     */
    public Tip getTip() {
        return tip;
    }

    /**
     * Munkaterület lekérdezése.
     *
     * @return munkaterület
     */
    public WorkingArea getWorkingArea() {
        return workingArea;
    }

    /**
     * Karok hosszának a lekérdezése.
     *
     * @return karok hossza
     */
    public double getArmsLength() {
        return armsLength;
    }

    /**
     * Menetes rudak közti távolságának lekérdezése.
     *
     * @return menetes rudak közti távolság
     */
    public double getThRodsDistance() {
        return thRodsDistance;
    }

    /**
     * Menetes rudak hosszának a lekérdezése.
     *
     * @return menetes rudak hossza
     */
    public double getThRodsLength() {
        return thRodsLength;
    }

    /**
     * Menetes rudak átmérőjének a lekérdezése.
     *
     * @return menetes rudak átmérője
     */
    public double getThRodDiameter() {
        return thRodDiameter;
    }

    /**
     * Menetes rudak menetemelkedésének a lekérdezése.
     *
     * @return menetes rudak menetemelkedése
     */
    public double getThRodPitch() {
        return thRodPitch;
    }

    /**
     * Percenkénti fordulatok számának a lekérdezése.
     *
     * @return percenkénti fordulatok száma
     */
    public double getRotationsPerMinute() {
        return rotationsPerMinute;
    }

    /**
     * Fordulatonkénti lépések számának a lekérdezése.
     *
     * @return fordulatonkénti lépések száma
     */
    public double getStepsPerRotation() {
        return stepsPerRotation;
    }

    /**
     * Fejtől való távolság lekérdezése.
     *
     * @return fejtől való távolság
     */
    public double getHeadDistance() {
        return headDistance;
    }

    /**
     * Csavartól való távolság lekérdezése.
     *
     * @return csavartól való távolság
     */
    public double getNutDistance() {
        return nutDistance;
    }

    /**
     * Gép klónozása.
     *
     * @return klón
     */
    public Machine getClone() {

        Machine m = new Machine(
                getThRodsDistance(),
                getThRodsLength(),
                getThRodDiameter(),
                getThRodPitch(),
                getRotationsPerMinute(),
                getStepsPerRotation(),
                getArmsLength(),
                getHeadDistance(),
                getNutDistance()
        );

        m.getWorkingArea().storeCorner1(getWorkingArea().getCorner1StepsAbsolute()[0], getWorkingArea().getCorner1StepsAbsolute()[1]);
        m.getWorkingArea().storeCorner2(getWorkingArea().getCorner2StepsAbsolute()[0], getWorkingArea().getCorner2StepsAbsolute()[1]);

        m.disableMotorActions();

        return m;
    }

    /**
     * Motorok adott számú léptetése.
     *
     * @param stepsLeft bal motor lépéseinek a száma
     * @param stepsRight jobb motor lépéseinek a száma
     */
    public void doMotorSteps(final double stepsLeft, final double stepsRight) {

        Thread mlt = new Thread(new Runnable() {
            @Override
            public void run() {
                getMotorLeft().doSteps(stepsLeft);
            }
        });
        Thread mrt = new Thread(new Runnable() {

            @Override
            public void run() {
                getMotorRight().doSteps(stepsRight);
            }
        });

        mlt.start();
        mrt.start();
        try {
            mlt.join();
            mrt.join();
        } catch (InterruptedException ex) {
        }

    }

    /**
     * Motorok léptetése az adott számú lépéshez.
     *
     * @param leftTo bal motor célja
     * @param rightTo jobb motor célja
     */
    public void doMotorStepsTo(double leftTo, double rightTo) {
        doMotorSteps(
                leftTo - getMotorLeft().getCurrentStepsAbsolute(),
                rightTo - getMotorRight().getCurrentStepsAbsolute()
        );
    }

    /**
     * Motorok parancsainak a letiltása.
     */
    public void disableMotorActions() {
        for (String n : nuts.keySet()) {
            getMotor(n).disableMotorAction();
        }
    }

    /**
     * Lyuk tesztelő metódus csatolása.
     *
     * @param ht lyuk tesztelő
     */
    public void setHoleTestMethod(final HoleTester ht) {
        holeTester = ht;
    }

    /**
     * Lyuk tesztelése az aktuális helyen.
     *
     * @return lyuk esetén igaz egyébként hamis
     */
    public boolean testHole() {

        if (null == holeTester) {
            return false;
        }

        return holeTester.isHole(getMotorLeft().getCurrentStepsAbsolute(), getMotorRight().getCurrentStepsAbsolute());
    }

}
